// SPDX-FileCopyrightText: Copyright 2025-2025 深圳市同心圆网络有限公司
// SPDX-License-Identifier: GPL-3.0-only

package watch_impl

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"time"

	"gitcode.com/openseaotter/so_agent/config"
	"gitcode.com/openseaotter/so_agent/utils"
	"gitcode.com/openseaotter/so_proto_gen_go.git/watch_api"
	"github.com/dchest/uniuri"
	"go.uber.org/zap"
)

type WatchRunner struct {
	maxExecSecond uint32
	watchCfg      config.WatchConfig
	logger        *zap.Logger

	runnerCtx    context.Context
	cancelFunc   context.CancelFunc
	running      bool
	lastChangeId string
	lastCallId   string
}

func NewWatchRunner(maxExecSecond uint32, watchCfg config.WatchConfig, logger *zap.Logger) *WatchRunner {
	ctx, cancel := context.WithCancel(context.Background())
	return &WatchRunner{
		maxExecSecond: maxExecSecond,
		watchCfg:      watchCfg,
		logger:        logger,

		runnerCtx:  ctx,
		cancelFunc: cancel,
		running:    false,
	}
}

func (wr *WatchRunner) GetWatchId() string {
	return wr.watchCfg.WatchId
}

func (wr *WatchRunner) Run() {
	wr.running = true
	for {
		if !wr.running {
			break
		}
		err := wr.runWatch()
		if err != nil {
			wr.logger.Error("has error", zap.String("watchId", wr.watchCfg.WatchId), zap.Error(err))
			time.Sleep(10 * time.Second)
		}
	}
}

func (wr *WatchRunner) runWatch() error {
	conn, err := utils.ConnGrpcServer(wr.watchCfg.ServerAddr)
	if err != nil {
		return err
	}
	defer conn.Close()

	client := watch_api.NewWatchAgentApiClient(conn)

	//等待change
	timeRes, err := client.GetTime(wr.runnerCtx, &watch_api.AgentGetTimeRequest{})
	if err != nil {
		return err
	}
	randomStr := uniuri.NewLenChars(32, []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"))
	signStr := utils.GenSign(timeRes.TimeStamp, randomStr, wr.watchCfg.Secret)

	watchRes, err := client.WatchChange(wr.runnerCtx, &watch_api.AgentWatchChangeRequest{
		WatchId:   wr.watchCfg.WatchId,
		TimeStamp: timeRes.TimeStamp,
		RandomStr: randomStr,
		Sign:      signStr,
	})
	if err != nil {
		return err
	}
	if watchRes.Code != watch_api.AgentWatchChangeResponse_CODE_OK {
		return fmt.Errorf("%s", watchRes.ErrMsg)
	}
	if watchRes.ChangeId == "" {
		return nil
	}
	if wr.lastChangeId == watchRes.ChangeId && wr.lastCallId == watchRes.CallId {
		return nil
	}
	wr.logger.Info("recv change", zap.String("watchId", wr.watchCfg.WatchId), zap.String("changeId", watchRes.ChangeId))
	//获取Change
	timeRes, err = client.GetTime(wr.runnerCtx, &watch_api.AgentGetTimeRequest{})
	if err != nil {
		return err
	}
	randomStr = uniuri.NewLenChars(32, []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"))
	signStr = utils.GenSign(timeRes.TimeStamp, randomStr, wr.watchCfg.Secret)

	changeRes, err := client.GetChange(wr.runnerCtx, &watch_api.AgentGetChangeRequest{
		WatchId:   wr.watchCfg.WatchId,
		TimeStamp: timeRes.TimeStamp,
		RandomStr: randomStr,
		Sign:      signStr,
		ChangeId:  watchRes.ChangeId,
	})
	if err != nil {
		return err
	}
	if changeRes.Code != watch_api.AgentGetChangeResponse_CODE_OK {
		return fmt.Errorf("%s", changeRes.ErrMsg)
	}
	err = wr.execScript(changeRes.Change)
	if err != nil {
		return err
	}
	wr.lastChangeId = watchRes.ChangeId
	wr.lastCallId = watchRes.CallId
	return nil
}

func (wr *WatchRunner) execScript(changeInfo *watch_api.ChangeInfo) error {
	wr.logger.Info("changeInfo", zap.String("watchId", wr.watchCfg.WatchId), zap.String("fromUrl", changeInfo.ChangeFromUrl), zap.String("toUrl", changeInfo.ChangeToUrl),
		zap.String("fromDigest", changeInfo.ChangeFromDigest), zap.String("toDigest", changeInfo.ChangeToDigest))

	if len(changeInfo.UnChangeUrlList) > 0 {
		wr.logger.Info("unchangeInfo list", zap.String("watchId", wr.watchCfg.WatchId), zap.Strings("unChangeList", changeInfo.UnChangeUrlList))
	}

	execCtx, cancel := context.WithTimeout(wr.runnerCtx, time.Duration(wr.maxExecSecond)*time.Second)
	defer cancel()

	jsonData, err := json.MarshalIndent(changeInfo, "", "  ")
	if err != nil {
		return err
	}

	//写入临时文件
	f, err := os.CreateTemp("", "sa_*.json")
	if err != nil {
		return err
	}
	defer f.Close()
	defer os.Remove(f.Name())

	_, err = f.Write(jsonData)
	if err != nil {
		return err
	}
	err = f.Sync()
	if err != nil {
		return err
	}

	stdoutRead, stdoutWrite, err := os.Pipe()
	if err != nil {
		return err
	}
	defer stdoutRead.Close()
	defer stdoutWrite.Close()

	go wr.readExecOut(stdoutRead)

	stderrRead, stderrWrite, err := os.Pipe()
	if err != nil {
		return err
	}
	defer stderrRead.Close()
	defer stderrWrite.Close()

	go wr.readExecOut(stderrRead)

	cmd := exec.CommandContext(execCtx, wr.watchCfg.ScriptFile, f.Name())
	cmd.Env = []string{
		"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
	}
	cmd.Stdout = stdoutWrite
	cmd.Stderr = stderrWrite
	return cmd.Run()
}

func (wr *WatchRunner) Stop() {
	if wr.running {
		wr.running = false
		wr.cancelFunc()
	}
}

func (wr *WatchRunner) readExecOut(f *os.File) {
	reader := bufio.NewReader(f)
	for {
		line, err := reader.ReadString('\n')
		if len(line) != 0 {
			wr.logger.Debug("script out", zap.String("watchId", wr.watchCfg.WatchId), zap.String("line", line))
		}
		if err != nil {
			break
		}
	}
}
